﻿///////////////////////////////////////////////////////////////////////////////
//	Copyright 2013 JASDev International
//
//	Licensed under the Apache License, Version 2.0 (the "License");
//	you may not use this file except in compliance with the License.
//	You may obtain a copy of the License at
//
//		http://www.apache.org/licenses/LICENSE-2.0
//
//	Unless required by applicable law or agreed to in writing, software
//	distributed under the License is distributed on an "AS IS" BASIS,
//	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//	See the License for the specific language governing permissions and
//	limitations under the License.
///////////////////////////////////////////////////////////////////////////////

using System;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Threading;

using JDI.Common.Logger;
using JDI.Common.Collections;
using JDI.Common.Utils;
using JDI.WebSocket.Client;

namespace JDI.Pusher.Client
{
	/// <summary>
	/// Pusher client class.
	/// </summary>
	public class PusherClient : IDisposable
	{
		#region Constructors and IDisposable

		/// <summary>
		/// Pusher constructor.
		/// </summary>
		/// <param name="logSourceID"></param>
		/// <param name="options">PusherOptions collection.</param>
		public PusherClient(string logSourceID, PusherOptions options = null)
		{
			this.loggerID = logSourceID;
			this.appKey = "";
			this.pusherState = PusherState.Initialized;
			this.socketID = null;
			this.channels = new Hashtable();
			this.eventEmitter = new PusherEventEmitter();
			this.eventLock = new object();

			this.options = options;
			if (this.options == null)
				this.options = new PusherOptions();

			this.authEndpoint = this.options.AuthEndpoint;
			this.authTransport = this.options.AuthTransport;

			WSOptions wsOptions = new WSOptions();
			wsOptions.MaskingEnabled = this.options.MaskingEnabled;
			wsOptions.Origin = this.options.Origin;
			wsOptions.ActivityTimerEnabled = this.options.ActivityTimerEnabled;
			wsOptions.ActivityTimeout = this.options.ActivityTimeout;
			
			this.webSocket = new WebSocketClient(this.loggerID + ":WebSocket", wsOptions);

			this.webSocket.ConnectionChanged += new WSDelegates.ConnectionChangedEventHandler(webSocket_ConnectionChanged);
			this.webSocket.TextMessageReceived += new WSDelegates.TextMessageReceivedEventHandler(webSocket_TextMessageReceived);
			this.webSocket.Error += new WSDelegates.ErrorEventHandler(webSocket_Error);
		}

		~PusherClient()
		{
			this.Dispose(false);
		}

		public void Dispose()
		{
			this.Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (this.isDisposed)
				return;

			if (disposing)
			{
                // disconnect
                this.Disconnect();
				DateTime timeoutTime = DateTime.Now.AddSeconds(5);
				while (this.pusherState != PusherState.Disconnected && DateTime.Now < timeoutTime)
					Thread.Sleep(500);

				// cleanup
				if (this.eventEmitter != null)
				{
					this.eventEmitter.Dispose();
				}

				if (this.channels != null)
				{
					foreach (DictionaryEntry entry in this.channels)
					{
						((PusherChannel)entry.Value).Dispose();
					}
					this.channels.Clear();
				}

				if (this.webSocket != null)
				{
					this.webSocket.ConnectionChanged -= new WSDelegates.ConnectionChangedEventHandler(webSocket_ConnectionChanged);
					this.webSocket.TextMessageReceived -= new WSDelegates.TextMessageReceivedEventHandler(webSocket_TextMessageReceived);
					this.webSocket.Error -= new WSDelegates.ErrorEventHandler(webSocket_Error);
					this.webSocket.Dispose();
				}
			}

			// set everything to null
			this.loggerID = null;
			this.appKey = null;
			this.authEndpoint = null;
			this.authTransport = null;
			this.socketID = null;
			this.eventLock = null;
			this.webSocket = null;
			this.channels = null;
			this.eventEmitter = null;
			this.options = null;

			this.Connected = null;
			this.ConnectionChanged = null;
			this.Disconnected = null;
			this.Error = null;

			// make sure we don't dispose again
			this.isDisposed = true;
		}

		#endregion


		#region Properties

		/// <summary>
		/// Get the current state of the PusherClient.
		/// </summary>
		public PusherState State
		{
			get { return this.pusherState; }
		}

		/// <summary>
		/// Get the Application Key value.
		/// </summary>
		public string AppKey
		{
			get { return this.appKey; }
		}

		/// <summary>
		/// Gets/sets the authentication endpoint on your server.
		/// </summary>
		public string AuthEndpoint
		{
			get { return this.authEndpoint; }
		}

		/// <summary>
		/// Gets/sets the authentication transport type on your server.
		/// </summary>
		public string AuthTransport
		{
			get { return this.authTransport; }
		}

		/// <summary>
		/// Gets the Socket ID of the current Pusher connection.
		/// </summary>
		public string SocketID
		{
			get { return this.socketID; }
		}

		/// <summary>
		/// Get the Library version.
		/// </summary>
		public string LibraryVersion
		{
			get { return PusherConst.LibraryVersion; }
		}

		/// <summary>
		/// Get the Pusher protocol version.
		/// </summary>
		public string ProtocolVersion
		{
			get { return PusherConst.ProtocolVersion; }
		}

		#endregion


		#region Methods

		/// <summary>
		/// Connect - Begins the asynchronous (non-blocking) connection process.
		/// </summary>
		/// <param name="appKey">Secret pusher application key.</param>
		public void Connect(string appKey)
		{
			if (this.pusherState == PusherState.Connected || this.pusherState == PusherState.Connecting)
				return;

			this.appKey = appKey;

			string scheme = (this.options.EncryptionEnabled ? "wss" : "ws");
			string port = (this.options.EncryptionEnabled ? "443" : "80");
			string query = string.Concat("?client=", PusherConst.PusherClient, "&version=", PusherConst.LibraryVersion, "&protocol=", PusherConst.ProtocolVersion);
			string url = string.Concat(scheme, "://ws.pusherapp.com:", port, "/app/", this.appKey, query);

			Logger.WriteDebug(this.loggerID, "Connecting...");
			this.webSocket.Connect(url);
		}

		/// <summary>
		/// Disconnect - Begins the asynchronous (non-blocking) disconnection process.
		/// </summary>
		public void Disconnect()
		{
			if (this.pusherState == PusherState.Initialized)
			{
				this.pusherState = PusherState.Disconnected;
			}
			else if (this.pusherState == PusherState.Connecting || this.pusherState == PusherState.Connected)
			{
				Logger.WriteDebug(this.loggerID, "Disconnecting...");
				this.webSocket.Disconnect();
			}
		}

		/// <summary>
		/// Gets channel by channel name.
		/// </summary>
		/// <param name="channelName">Name of channel.</param>
		/// <returns>Channel if found, or null.</returns>
		public PusherChannel GetChannel(string channelName)
		{
			if (this.channels.Contains(channelName))
				return (PusherChannel)this.channels[channelName];
			return null;
		}

		/// <summary>
		/// Subscribes to a channel.
		/// </summary>
		/// <param name="channelName">Name of channel.</param>
		/// <param name="callback">Method to call when channel status changes.</param>
		/// <returns>Pusher channel.</returns>
		public PusherChannel Subscribe(string channelName, PusherDelegates.ChannelStatusCallback callback = null)
		{
			if (this.pusherState != PusherState.Connected)
			{
				this.OnError("Pusher must be connected before calling Subscribe.");
				return null;
			}

			if (this.channels.Contains(channelName))
			{
				return (PusherChannel)this.channels[channelName];
			}

			PusherChannel newChannel = null;

			if (channelName.ToLower().IndexOf(PusherConst.ChannelPrefix_Presence) == 0)
			{
				newChannel = new PusherPresenceChannel(channelName, this);
			}
			else if (channelName.ToLower().IndexOf(PusherConst.ChannelPrefix_Private) == 0)
			{
				newChannel = new PusherPrivateChannel(channelName, this);
			}
			else
			{
				newChannel = new PusherChannel(channelName, this);
			}

			this.channels.Add(channelName, newChannel);
			newChannel.Subscribe(callback);

			return newChannel;
		}

		/// <summary>
		///  Unsubscribes from a channel.
		/// </summary>
		/// <param name="channelName">Name of channel.</param>
		public void UnSubscribe(string channelName)
		{
			if (this.pusherState != PusherState.Connected)
			{
				this.OnError("Pusher must be connected before calling UnSubscribe.");
				return;
			}

			PusherChannel channel = null;
			if (this.channels.Contains(channelName))
			{
				// get channel
				channel = (PusherChannel)this.channels[channelName];

				// send message
				channel.UnSubscribe();

				// remove channel
				this.channels.Remove(channelName);
				channel.Dispose();
				channel = null;
			}
		}

		/// <summary>
		///  Unsubscribes from all channels.
		/// </summary>
		public void UnSubscribeAll(string channelName)
		{
			if (this.pusherState != PusherState.Connected)
			{
				this.OnError("Pusher must be connected before calling UnSubscribeAll.");
				return;
			}

			PusherChannel channel = null;
			foreach (DictionaryEntry entry in this.channels)
			{
				// get channel
				channel = (PusherChannel)entry.Value;

				// send message
				channel.UnSubscribe();

				// dispose channel
				channel.Dispose();
			}

			// remove all channels
			this.channels.Clear();
		}

		/// <summary>
		/// Sends a JSON encoded Pusher event message.
		/// </summary>
		/// <param name="eventMessage">JSON string representation of the Pusher event message.</param>
		/// <returns>true if successful, false otherwise.</returns>
		public bool SendMessage(string eventMessage)
		{
			if (this.pusherState != PusherState.Connected)
			{
				this.OnError("Pusher must be connected before calling Send.");
				return false;
			}

			Logger.WriteDebug(this.loggerID, "Send: " + eventMessage);
			this.webSocket.SendText(eventMessage);
			return true;
		}

		#endregion


		#region EventEmitter Methods

		public void Bind(string eventName, PusherDelegates.BindEventCallback callback)
		{
			this.eventEmitter.Bind(eventName, callback);
		}

		public void BindAll(PusherDelegates.BindEventCallback callback)
		{
			this.eventEmitter.BindAll(callback);
		}

		public void UnBind(string eventName, PusherDelegates.BindEventCallback callback)
		{
			this.eventEmitter.UnBind(eventName, callback);
		}

		public void UnBindAll(PusherDelegates.BindEventCallback callback)
		{
			this.eventEmitter.UnBindAll(callback);
		}

		#endregion


		#region Events

		public event PusherDelegates.ConnectionChangedEventHandler ConnectionChanged;
		public event PusherDelegates.ConnectedEventHandler Connected;
		public event PusherDelegates.DisconnectedEventHandler Disconnected;
		public event PusherDelegates.ErrorEventHandler Error;

		#endregion


		#region Event Invoke Methods

		protected void OnConnectionChanged(string eventData = null)
		{
			PusherDelegates.ConnectionChangedEventHandler eventHandler = null;
			lock (this.eventLock)
			{
				eventHandler = this.ConnectionChanged;
			}
			if (eventHandler != null)
			{
				eventHandler(this.pusherState);
			}
		}

		protected void OnConnected()
		{
			PusherDelegates.ConnectedEventHandler eventHandler = null;
			lock (this.eventLock)
			{
				eventHandler = this.Connected;
			}
			if (eventHandler != null)
			{
				eventHandler();
			}
		}

		protected void OnDisconnected()
		{
			PusherDelegates.DisconnectedEventHandler eventHandler = null;
			lock (this.eventLock)
			{
				eventHandler = this.Disconnected;
			}
			if (eventHandler != null)
			{
				eventHandler();
			}
		}

		protected void OnError(string message, string stackTrace = null)
		{
			PusherDelegates.ErrorEventHandler eventHandler = null;
			lock (this.eventLock)
			{
				eventHandler = this.Error;
			}
			if (eventHandler != null)
			{
				eventHandler(message, stackTrace);
			}
		}

		#endregion


		#region Internal Methods

		internal bool SendSubscribeRequest(string channelName)
		{
			// {"event":"pusher:subscribe", "data": {"channel":String}}
			string eventMessage = string.Concat("{\"event\": \"", PusherConst.EventName_Subscribe, "\", \"data\": {\"channel\": \"", channelName, "\"}}");
		//	string eventMessage = "{\"event\": \"pusher:subscribe\",\"data\": {\"channel\": \"default\" }}";
			return this.SendMessage(eventMessage);
		}

		internal bool SendSubscribeRequest(string channelName, string authSignature, string channelData = null)
		{
			// {"event": "pusher:subscribe", "data": {"channel": String, "auth": String, "channel_data": Object}}
			string eventMessage = string.Concat("{\"event\": \"", PusherConst.EventName_Subscribe, "\", \"data\": {\"channel\": \"", channelName, "\", \"auth\": \"", authSignature, "\"", (StringUtil.IsNullOrEmpty(channelData) ? "" : ", \"channel_data\": " + channelData), "}}");
			return this.SendMessage(eventMessage);
		}

		internal bool SendUnSubscribeRequest(string channelName)
		{
			// {"event": "pusher:unsubscribe", "data" : {"channel": String}}
			string eventMessage = string.Concat("{\"event\": \"", PusherConst.EventName_UnSubscribe, "\", \"data\": {\"channel\": \"", channelName, "\"}}");
			return this.SendMessage(eventMessage);
		}

		internal bool SendClientEvent(string channelName, string eventName, string eventData = null)
		{
			// {"event": String, "channel": String, "data": Hash}
			string eventMessage = string.Concat("{\"event\": \"", eventName, "\", \"channel\": \"", channelName, "\"", (StringUtil.IsNullOrEmpty(eventData) ? "" : ", \"data\": " + eventData), "}");
			return this.SendMessage(eventMessage);
		}

		#endregion


		#region WebSocket Event Handlers

		protected void webSocket_ConnectionChanged(WebSocketState websocketState)
		{
			switch (websocketState)
			{
				case WebSocketState.Initialized:
					// do nothing
					break;
				case WebSocketState.Connecting:
					this.pusherState = PusherState.Connecting;
					this.OnConnectionChanged();
					break;
				case WebSocketState.Connected:
					// do nothing
					break;
				case WebSocketState.Disconnecting:
					this.pusherState = PusherState.Disconnecting;
					this.OnConnectionChanged();
					break;
				case WebSocketState.Disconnected:
					this.pusherState = PusherState.Disconnected;
					this.OnConnectionChanged();
					this.OnDisconnected();
					break;
			}
		}

		protected void webSocket_TextMessageReceived(string textMessage)
		{
			// TODO - handle "pusher_internal:disconnected" message
			// TODO - handle "pusher_internal:authorized" message

			// TODO - fire pusher:<eventname> events when pusher_internal:<eventname> events are received

			// parse incoming message
			PusherMessage pusherMessage = PusherMessage.TryParse(textMessage);
			if (pusherMessage == null)
			{
				Logger.WriteError(this.loggerID, "Unable to parse incoming message: " + textMessage);
				return;
			}

			// publish message to listeners
			this.eventEmitter.EmitEvent(pusherMessage.Name, pusherMessage.RawData);

			// handle message
			switch (pusherMessage.Type)
			{
				case PusherConst.EventType.ConnectionEstablished:
					var connectionData = (PusherMessage.ConnectionData)pusherMessage.Data;
					this.socketID = connectionData.SocketID;
					this.pusherState = PusherState.Connected;
					this.OnConnectionChanged();
					this.OnConnected();
					break;

				case PusherConst.EventType.SubscriptionSucceeded:
					if (this.channels.Contains(pusherMessage.Channel))
					{
						var channel = ((PusherChannel)this.channels[pusherMessage.Channel]);
						if (channel.Type == PusherChannelType.Presence)
						{
							// TODO - get member list
						}
						channel.SubscriptionSucceeded((string)pusherMessage.Data);
					}
					break;

				case PusherConst.EventType.SubscriptionError:
					if (this.channels.Contains(pusherMessage.Channel))
					{
						var channel = ((PusherChannel)this.channels[pusherMessage.Channel]);
						channel.SubscriptionFailed((string)pusherMessage.Data);
					}
					break;

				case PusherConst.EventType.MemberAdded:
					if (this.channels.Contains(pusherMessage.Channel))
					{
						var channel = ((PusherPresenceChannel)this.channels[pusherMessage.Channel]);
						channel.AddMember((PusherMessage.MemberData)pusherMessage.Data);
					}
					break;

				case PusherConst.EventType.MemberRemoved:
					if (this.channels.Contains(pusherMessage.Channel))
					{
						var channel = ((PusherPresenceChannel)this.channels[pusherMessage.Channel]);
						channel.RemoveMember((string)pusherMessage.Data);
					}
					break;


				case PusherConst.EventType.Error:
					var errorData = (PusherMessage.ErrorData)pusherMessage.Data;
					this.OnError(string.Concat(errorData.Message, " (", errorData.Code, ")"));
					break;

				default:
					if (this.channels.Contains(pusherMessage.Channel))
					{
						var channel = ((PusherChannel)this.channels[pusherMessage.Channel]);
						channel.EmitEvent(pusherMessage.Name, pusherMessage.RawData);
					}
					break;
			}

			if (pusherMessage != null)
			{
				pusherMessage.Dispose();
				pusherMessage = null;
			}

		}

		protected void webSocket_Error(string message, string stackTrace)
		{
			this.OnError(message, stackTrace);
		}

		#endregion


		#region Member Fields

		protected bool isDisposed;

		protected string loggerID;
		protected string appKey;

		protected string authEndpoint;
		protected string authTransport;

		protected PusherOptions options;

		protected string socketID;

		protected PusherState pusherState;
		protected Hashtable channels;
		private PusherEventEmitter eventEmitter;

		protected object eventLock;

		protected WebSocketClient webSocket;

		#endregion
	}
}
